﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Xml;
using System.Xml.XPath;

namespace XmlDSigEx
{
    public class XPathFilter2Transform : ITransform
    {
        public const string URI_XPATH2TRANSFORM = "http://www.w3.org/2002/06/xmldsig-filter2";
        private const string FILTER_INTERSECT = "intersect";
        private const string FILTER_SUBTRACT = "subtract";
        private const string FILTER_UNION = "union";

        private XmlDocument _documentContext;

        private List<XPathFilter2Expression> _expressionList = new List<XPathFilter2Expression>();

        protected List<XPathFilter2Expression> ExpressionList
        {
            get { return _expressionList; }
            set { _expressionList = value; }
        }

        public XPathFilter2Transform(XmlDocument documentContext)
        {
            _documentContext = documentContext;
        }
        public bool SupportsInputType(Type inputType)
        {
            return inputType == typeof(List<XmlNode>);
        }

        public virtual object Process(object input,  XmlElement transformElement)
        {
            List<XmlNode> inputDocument = input as List<XmlNode>;
            if (inputDocument != null)
            {
                List<XmlNode> F = new List<XmlNode>(inputDocument);

                XmlNamespaceManager mgr = new XmlNamespaceManager(new NameTable());
                mgr.AddNamespace("xp", URI_XPATH2TRANSFORM);

                List<XmlElement> xPathElements = new List<XmlElement>(transformElement.GetElementsByTagName("XPath", URI_XPATH2TRANSFORM).OfType<XmlElement>());
                foreach (XmlElement xPathElement in xPathElements)
                {
                    XPathFilter2Expression expression = AddExpression(FilterTypeFromString(xPathElement.Attributes["Filter"].Value), xPathElement.InnerText);
                    XPathNavigator xpathElementNavigator = xPathElement.CreateNavigator();
                    foreach (KeyValuePair<string, string> nsPair in xpathElementNavigator.GetNamespacesInScope(XmlNamespaceScope.All))
                    {
                        //Note hack here to make stupid, incorrect adobe signatures work...
                        if(nsPair.Key != "")
                        expression.AddNamespace(nsPair.Key, nsPair.Value);
                    }
                    F = ProcessXPathElement(xpathElementNavigator, F, inputDocument, expression);
                }
                return new List<XmlNode>(inputDocument.Intersect(F));
            }
            throw new ArgumentException("The XPathFilter2 only supports an input type of List<XmlNode>");
        }

        protected List<XmlNode> ProcessXPathElement(XPathNavigator xPathContext, List<XmlNode> F, List<XmlNode> inputDocument, XPathFilter2Expression expression)
        {
            XPathContext context = new XPathContext(new NameTable(), xPathContext.UnderlyingObject as XmlElement);

            foreach (KeyValuePair<string, string> ns in expression.Namespaces)
            {
                context.AddNamespace(ns.Key, ns.Value);
            }
            XPathExpression xPath = xPathContext.Compile(expression.Expression);
            xPath.SetContext(context);
            XPathNodeIterator nodeIterator = _documentContext.CreateNavigator().Select(".");
            var S = xPathContext.Evaluate(xPath, nodeIterator) as XPathNodeIterator;
            if (S == null)
            {
                throw new ArgumentException("Expected a nodeset to be returned from the XPath expression.");
            }

            List<XmlNode> S2 = new List<XmlNode>();
            foreach (XmlNode node in inputDocument)
            {
                foreach (XPathNavigator resultNode in S)
                {
                    if (node == resultNode.UnderlyingObject || resultNode.IsDescendant(node.CreateNavigator()))
                    {
                        S2.Add(node);
                        break;
                    }
                }
            }
            return ProcessFilter(expression.FilterType, F, S2);
        }

        private FilterType FilterTypeFromString(string filter)
        {
            switch (filter)
            {
                case FILTER_INTERSECT:
                    return FilterType.Intersect;
                case FILTER_SUBTRACT:
                    return FilterType.Subtract;
                case FILTER_UNION:
                    return FilterType.Union;

                default:
                    throw new ArgumentException("Unknown filter type.");
            }
        }

        private List<XmlNode> ProcessFilter(FilterType type, List<XmlNode> F, List<XmlNode> S2)
        {
            List<XmlNode> output = new List<XmlNode>(F);

            switch (type)
            {
                case FilterType.Intersect:
                    output = new List<XmlNode>(F.Intersect(S2));
                    return output;

                case FilterType.Subtract:
                    foreach (XmlNode node in S2)
                    {
                        output.Remove(node);
                    }
                    return output;

                case FilterType.Union:
                    foreach (XmlNode node in S2)
                    {
                        if (!output.Contains(node))
                        {
                            output.Add(node);
                        }
                    }
                    return output;
            }

            throw new ArgumentException("Encountered unexpected XPath filter type");
        }


        public virtual void WriteXml(XmlWriter writer)
        {
            foreach (XPathFilter2Expression expression in _expressionList)
            {
                writer.WriteStartElement("ds-xp", "XPath", URI_XPATH2TRANSFORM);
                foreach (KeyValuePair<string, string> ns in expression.Namespaces)
                {
                    writer.WriteAttributeString("xmlns", ns.Key, null, ns.Value);
                }
                string filter = null;
                switch (expression.FilterType)
                {
                    case FilterType.Intersect:
                        filter = FILTER_INTERSECT;
                        break;
                    case FilterType.Subtract:
                        filter = FILTER_SUBTRACT;
                        break;
                    case FilterType.Union:
                        filter = FILTER_UNION;
                        break;
                    default:
                        throw new ArgumentOutOfRangeException("Unexpected filter type encountered.");
                }
                writer.WriteAttributeString("Filter", filter);
                writer.WriteString(expression.Expression);
                writer.WriteEndElement();
            }
        }

        public virtual string Uri
        {
            get
            {
                return URI_XPATH2TRANSFORM;
            }
        }

        public XPathFilter2Expression AddExpression(FilterType filterType, string expression)
        {
            var expressionObject = new XPathFilter2Expression() { FilterType = filterType, Expression = expression };
            _expressionList.Add(expressionObject);
            return expressionObject;
        }
    }

    public enum FilterType
    {
        Intersect,
        Subtract,
        Union
    }

    public class XPathFilter2Expression
    {
        private Dictionary<string, string> _Namespaces = new Dictionary<string, string>();

        internal XPathFilter2Expression()
        {
        }

        public FilterType FilterType { get; set; }
        public string Expression { get; set; }
        public void AddNamespace(string prefix, string uri)
        {
            _Namespaces.Add(prefix, uri);
        }

        internal Dictionary<string, string> Namespaces { get { return _Namespaces; } }

    }
}
